package edu.iscas.xcheck.analysis.dom.utils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;

import edu.iscas.xcheck.analysis.dom.model.BrowserEvent;
import edu.iscas.xcheck.analysis.dom.model.BrowserPage;
import edu.iscas.xcheck.analysis.dom.model.ChangedNode;
import edu.iscas.xcheck.analysis.dom.model.DomNode;
import edu.iscas.xcheck.analysis.dom.model.LocalPage;
import edu.iscas.xcheck.analysis.dom.model.MutationEvent;
import edu.iscas.xcheck.analysis.model.Platform;

public class DataProcessor_state {

	public static void processData(Map<Platform, List<BrowserPage>> dataMap,
			StateEdgeUtil seUtil) {
		for (Platform platform:dataMap.keySet()) {
			List<BrowserPage> browserLogs = dataMap.get(platform);
			for (BrowserPage log : browserLogs) {
				// one page
				processOnePage(seUtil, log.getLog().getEvents(), platform);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private static void processOnePage(StateEdgeUtil seUtil,
			List<BrowserEvent> eventList, Platform platform) {
		HashMap<String, DomNode> baseMap = null, cmpMap;
		// List<BrowserEvent> unmatched = new ArrayList();
		for (BrowserEvent event : eventList) {
			int checkResult = seUtil.checkState(event, platform);
			MutationEvent mutationEvent = event.getMutation();
			if (mutationEvent == null)
				continue;
			List<DomNode> domNodes = mutationEvent.getInit();
			if (domNodes == null)
				continue;
			HashMap<String, DomNode> idMap = new HashMap<>();
			convertList2Map(domNodes, idMap);
			mutationEvent.setInit(null);
			mutationEvent.setIdMap((Map<String, DomNode>) idMap.clone());
			if (baseMap != null) {
				// compare
				if(checkResult == 0){
					cmpMap = (HashMap<String, DomNode>) idMap.clone();
					ChangedNode changedNode = processOneEvent(baseMap, cmpMap);
					if (changedNode != null) {
						Map<String, LocalPage> localPageMap = getChangedArea(
								changedNode, idMap);
						mutationEvent.setLocalPageMap(localPageMap);
						mutationEvent.setRemoveMap(changedNode.getRemoveMap());
						mutationEvent.setAddMap(changedNode.getAddMap());
						mutationEvent.setAdded(changedNode.getAdded());
					}
				}else{
					event.setMutation(null);
				}
				baseMap = idMap;
			} else if (event.getType().equals("load")) {
				baseMap = idMap;
				DomNode root = domNodes.get(0);
				LocalPage localPage = new LocalPage(root, domNodes);
				Map<String, LocalPage> localPageMap = new HashMap<>();
				localPageMap.put(root.getNodeid(), localPage);
				mutationEvent.setLocalPageMap(localPageMap);
				mutationEvent.setAddMap((Map<String, DomNode>) idMap.clone());
				mutationEvent.setAdded(domNodes);
			} else {
				event.setMutation(null);
			}
		}
		// eventList.removeAll(unmatched);
		// return unmatched;
	}

	private static Map<String, LocalPage> getChangedArea(
			ChangedNode changedNode, Map<String, DomNode> idMap) {
		Map<String, Set<DomNode>> tree = new HashMap<>();
		Set<String> allChangedNode = new HashSet<>();
		buildTree(changedNode.getAdded(), tree, allChangedNode, idMap);
		buildTree(changedNode.getAffected(), tree, allChangedNode, idMap);
		buildTree(changedNode.getAttributeChanged(), tree, allChangedNode,
				idMap);
		// buildTree(changedNode.getRemoved(), tree, allChangedNode, idMap);
		buildTree(changedNode.getReparented(), tree, allChangedNode, idMap);
		buildTree(changedNode.getReordered(), tree, allChangedNode, idMap);
		// travel tree find local part
		List<String> localRoot = getLocalRoot(tree, allChangedNode);
		return getLocalPart(tree, localRoot, idMap);
	}

	private static Map<String, LocalPage> getLocalPart(
			Map<String, Set<DomNode>> tree, List<String> localRoot,
			Map<String, DomNode> idMap) {
		Map<String, LocalPage> localPart = new HashMap<>();
		for (String rootId : localRoot) {
			List<DomNode> changedList = getPartChanged(tree, idMap.get(rootId));
			LocalPage localPage = new LocalPage(idMap.get(rootId), changedList);
			localPart.put(rootId, localPage);
		}
		return localPart;
	}

	private static List<DomNode> getPartChanged(Map<String, Set<DomNode>> tree,
			DomNode root) {
		List<DomNode> changedList = new ArrayList<>();
		Queue<DomNode> queue = new LinkedList<>();
		queue.offer(root);
		while (!queue.isEmpty()) {
			DomNode node = queue.poll();
			Set<DomNode> childList = tree.get(node.getNodeid());
			if (childList != null) {
				changedList.addAll(childList);
				queue.addAll(childList);
			}
		}
		return changedList;
	}

	private static List<String> getLocalRoot(Map<String, Set<DomNode>> tree,
			Set<String> changedSet) {
		List<String> rootList = new ArrayList<>();
		Queue<String> queue = new LinkedList<>();
		String htmlId = "-1";
		queue.add(htmlId);
		while (!queue.isEmpty()) {
			String id = queue.poll();
			Set<DomNode> childNodeSet = tree.get(id);
			if (childNodeSet == null)
				continue;
			List<String> noChange = new ArrayList<>();
			boolean isChanged = false;
			for (DomNode child : childNodeSet) {
				if (changedSet.contains(child.getNodeid())) {
					rootList.add(child.getPid());
					isChanged = true;
					break;
				} else {
					noChange.add(child.getNodeid());
				}
			}
			if (!isChanged) {
				queue.addAll(noChange);
			}
		}
		return rootList;
	}

	private static void buildTree(List<DomNode> changedList,
			Map<String, Set<DomNode>> tree, Set<String> changedSet,
			Map<String, DomNode> idMap) {
		if (changedList == null || changedList.isEmpty())
			return;
		for (DomNode domNode : changedList) {
			String id = domNode.getNodeid();
			if (!tree.containsKey(id)) {
				insert2Tree(tree, domNode, idMap);
			}
			changedSet.add(id);
		}
	}

	private static void insert2Tree(Map<String, Set<DomNode>> tree,
			DomNode node, Map<String, DomNode> idMap) {
		Set<DomNode> childSet = new HashSet<>();
		tree.put(node.getNodeid(), childSet);
		String pid = node.getPid();
		while (!pid.equals("-2")) {// -2 is the pid of /html
			childSet = tree.get(pid);
			if (childSet == null) {
				childSet = new HashSet<>();
				tree.put(pid, childSet);
				childSet.add(node);
				node = idMap.get(pid);
				pid = node.getPid();
			} else {
				childSet.add(node);
				break;
			}
		}
	}

	/**
	 * @param baseMap
	 * @param cmpMap
	 * @return changed node
	 */
	private static ChangedNode processOneEvent(Map<String, DomNode> baseMap,
			Map<String, DomNode> cmpMap) {
		List<DomNode> added = new ArrayList<>(), removed = new ArrayList<>(), reordered = new ArrayList<>(), reparented = new ArrayList<>(), attributeChanged = new ArrayList<>(), affected = new ArrayList<>(), textChanged = new ArrayList<>();
		Map<String, DomNode> removeMap = new HashMap<>(), addMap = new HashMap<>();
		Iterator<Entry<String, DomNode>> it = baseMap.entrySet().iterator();
		while (it.hasNext()) {
			Entry<String, DomNode> entry = it.next();
			String nodeId = entry.getKey();
			DomNode baseNode = entry.getValue();
			DomNode cmpNode = cmpMap.get(nodeId);
			if (cmpNode == null) {
				removed.add(baseNode);
				removeMap.put(baseNode.getNodeid(), baseNode);
			} else {
				if (isTextChanged(baseNode, cmpNode)) {
					textChanged.add(cmpNode);
				} else if (isAttrChanged(baseNode, cmpNode)) {
					attributeChanged.add(cmpNode);
				} else if (baseNode.isTag()) {
					if (isAffected(baseNode, cmpNode)) {
						affected.add(cmpNode);
					} else if (!baseNode.getxPath().equals(cmpNode.getxPath())
							&& baseNode.getTagName().equals(
									cmpNode.getTagName())) {
						if (isSamePre(baseNode, cmpNode)) {
							reordered.add(cmpNode);
						} else {
							reparented.add(cmpNode);
						}
					}
				}
				cmpMap.remove(nodeId);
			}
			it.remove();
		}
		for (DomNode node : cmpMap.values()) {
			added.add(node);
			addMap.put(node.getHash(), node);
		}
		if (added.isEmpty() && removed.isEmpty() && affected.isEmpty()
				&& attributeChanged.isEmpty() && reordered.isEmpty()
				&& reparented.isEmpty()) {
			return null;
		} else {
			ChangedNode changedNode = new ChangedNode();
			Collections.sort(added, new NodeComparator());
			changedNode.setAdded(added);
			changedNode.setRemoved(removed);
			changedNode.setAffected(affected);
			changedNode.setAttributeChanged(attributeChanged);
			changedNode.setReordered(reordered);
			changedNode.setReparented(reparented);
			changedNode.setTextChanged(textChanged);
			changedNode.setRemoveMap(removeMap);
			changedNode.setAddMap(addMap);
			return changedNode;
		}
	}

	private static String getNoBlankText(String str) {
		if (str == null)
			return "";
		return str.replaceAll("[^a-zA-Z0-9]", "");
	}

	private static boolean isTextChanged(DomNode baseNode, DomNode cmpNode) {
		if (baseNode.isText() && cmpNode.isText()) {
			String baseText = getNoBlankText(baseNode.getText());
			String cmpText = getNoBlankText(cmpNode.getText());
			return baseText.equals(cmpText);
		}
		return false;
	}

	private static boolean isAttrChanged(DomNode baseNode, DomNode cmpNode) {
		Map<String, String> baseAttrMap = baseNode.getAttributes();
		Map<String, String> cmpAttrMap = cmpNode.getAttributes();
		if (baseAttrMap == null && cmpAttrMap == null)
			return false;
		if (baseAttrMap == null || cmpAttrMap == null)
			return true;
		if (baseAttrMap.size() != cmpAttrMap.size())
			return true;
		for (String key : baseAttrMap.keySet()) {
			String baseAttr = getNoBlankText(baseAttrMap.get(key));
			String cmpAttr = getNoBlankText(cmpAttrMap.get(key));
			if (baseAttr != null && !baseAttr.equals(cmpAttr))
				return true;
		}
		return false;
	}

	private static boolean isAffected(DomNode baseNode, DomNode cmpNode) {
		if (!LayoutUtils.isLayoutNode(baseNode)
				&& !LayoutUtils.isLayoutNode(cmpNode)) {
			return false;
		}
		int[] baseCoord = baseNode.getCoords();
		int[] cmpCoord = cmpNode.getCoords();
		for (int i = 0; i < 4; i++) {
			if (baseCoord[i] != cmpCoord[i])
				return true;
		}
		return false;
	}

	private static boolean isSamePre(DomNode baseNode, DomNode cmpNode) {
		String baseXPath = baseNode.getxPath();
		String cmpXPath = cmpNode.getxPath();
		int baseTagBegin = baseXPath.lastIndexOf("/");
		int cmpTagBegin = cmpXPath.lastIndexOf("/");
		if (baseTagBegin != -1 && cmpTagBegin != -1) {
			return baseXPath.substring(0, baseTagBegin).equals(
					cmpXPath.substring(0, cmpTagBegin));
		}
		return false;
	}

	// private static boolean isReorder(DomNode baseNode, DomNode cmpNode) {
	// return baseNode.getTagName().equals(cmpNode.getTagName())
	// && !baseNode.getOrder().equals(cmpNode.getOrder());
	// }
	//
	// private static boolean isReparent(DomNode baseNode, DomNode cmpNode) {
	// return baseNode.getTagName().equals(cmpNode.getTagName())
	// && baseNode.getOrder().equals(cmpNode.getOrder());
	// }

	private static void convertList2Map(List<DomNode> domNodes,
			Map<String, DomNode> idMap) {
		for (DomNode node : domNodes) {
			idMap.put(node.getNodeid(), node);
			DomNode parent = idMap.get(node.getPid());
			if (parent == null)
				continue;
			List<DomNode> childList = parent.getChildren();
			if (childList == null) {
				childList = new ArrayList<DomNode>();
				parent.setChildren(childList);
			}
			childList.add(node);
			node.setParent(parent);
			;
		}
	}

}
